package blue.squares;

import android.graphics.Color;
import android.graphics.Point;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.Arrays;

/**
 *
 * @author James Bruce
 */
public class Game {
    // ------------------------------------------------------------------------
    // Constants.
    // ------------------------------------------------------------------------

    static final String BUNDLE_BOARD_SIZE = "game.boardSize";
    static final String BUNDLE_BOARD = "game.board";
    static final String BUNDLE_CURRENT_MOVE = "game.currentMove";
    static final String BUNDLE_PLAYERS = "game.players";
    static final String BUNDLE_MOVES = "game.moves";
    static final String BUNDLE_SELECTED_X = "game.selected.x";
    static final String BUNDLE_SELECTED_Y = "game.selected.y";
    static final String BUNDLE_LAST_SQUARES = "game.lastSquares";
    
    public static final int HUMAN_PLAYER = 0;
    public static final int NOVICE_COMPUTER = 1;

    // ------------------------------------------------------------------------
    // Fields.
    // ------------------------------------------------------------------------

    // Board width and height (NxN).
    final int _boardSize;
    // The current game board state.  Each item is a move index.
    final int[][] _board;
    // Which move we're on [0,N*N-1].
    int _currentMove;
    // Last valid move index + 1 (equals N*N)
    int _maxMove;
    // List of players for this game.
    final Player[] _players;
    // List of every move in the game so far.
    final ArrayList<Move> _moves;
    // Currently selected location in grid coordinates.
    Point _selected;
    // Squares formed by piece at current x,y.
    Square[] _selectedSquares;

    // ------------------------------------------------------------------------
    // Implementation.
    // ------------------------------------------------------------------------

    public Game(int boardSize, int player2Type) {
        // must initialize board size first, many things depends on this
        _boardSize = boardSize;

        _players = new Player[] {
            new Player(0, Color.BLUE ),
            new Player(1, Color.GREEN),
        };

        switch(player2Type){
            case HUMAN_PLAYER: 
                // do nothing
                break;
                
            case NOVICE_COMPUTER:
                _players[1].setStrategy(new RandomStrategy(this, _players[1]));
                break;
        }

        // We ensure maxMove is even so that the game is more fair.
        _maxMove = (_boardSize * _boardSize) & ~1;
        _board = new int[_boardSize][_boardSize];
        _moves = new ArrayList<Move>();
        _currentMove = 0;
        _selected = new Point(0, 0);
    }

    //---- Methods for saving and loading game state ----//

    public Game(Bundle gameState){
        _boardSize = gameState.getInt(BUNDLE_BOARD_SIZE);
        _currentMove = gameState.getInt(BUNDLE_CURRENT_MOVE);
        _selected = new Point(gameState.getInt(BUNDLE_SELECTED_X),
                              gameState.getInt(BUNDLE_SELECTED_Y));
        _maxMove = (_boardSize * _boardSize) & ~1;

        ArrayList<Player> players = gameState.getParcelableArrayList(BUNDLE_PLAYERS);
        _players = players.toArray(new Player[players.size()]);  

        _board = to2DBoard(gameState.getIntArray(BUNDLE_BOARD));        
        _moves = gameState.getParcelableArrayList(BUNDLE_MOVES);
        select(_selected.x, _selected.y);
    }

    public Bundle getBundle(){
        Bundle bundle = new Bundle();
        bundle.putInt(BUNDLE_BOARD_SIZE, _boardSize);
        bundle.putInt(BUNDLE_CURRENT_MOVE, _currentMove);
        bundle.putInt(BUNDLE_SELECTED_X, _selected.x);
        bundle.putInt(BUNDLE_SELECTED_Y, _selected.y);

        ArrayList<Player> players = new ArrayList<Player>();
        players.addAll(Arrays.asList(_players));
        bundle.putParcelableArrayList(BUNDLE_PLAYERS, players);
        bundle.putIntArray(BUNDLE_BOARD, to1DBoard(_board));
        bundle.putParcelableArrayList(BUNDLE_MOVES, _moves);

        return bundle;
    }

    //---- Overriding base classes ----//

    @Override
    public String toString(){
        StringBuffer sb = new StringBuffer();
        sb.append("Game:{\n");
        sb.append("  BoardSize="+_boardSize+"\n");
        sb.append("  CurrentMove="+_currentMove+" Selected=("+_selected.x+","+_selected.y+")\n");
        for(int i = 0; i < _players.length; i++){
            sb.append("  Player["+i+"] "+_players[i]+"\n");
        }
        sb.append("  Board:\n");
        for(int r = 0; r < _board.length; r++){
            sb.append("  ");
            for(int c = 0; c < _board.length; c++){
                sb.append(_board[r][c]+" ");
            }
            sb.append("\n");
        }
        sb.append("  Moves="+Arrays.asList(_moves));
        sb.append("}\n");
        return sb.toString();
    }

    //---- Methods for accessing and modifying the board ----//

    public int getBoardSize(){
        return _boardSize;
    }

    private boolean isValidLocation(int x, int y){
        return x >= 0 && y >= 0 && x < _boardSize && y < _boardSize;
    }

    public Player getPlayerAtLoc(final Point p) {
        if(!hasPiece(p.x, p.y)) return null;
        return _players[_board[p.x][p.y] % _players.length];
    }

    public boolean isFree(int x, int y) {
        return isValidLocation(x, y) && _board[x][y] >= _currentMove;
    }

    public boolean hasPiece(int x, int y) {
        return isValidLocation(x, y) && _board[x][y] < _currentMove;
    }

    private void replacePieces(int src, int dest) {
        for(int x = 0; x < _boardSize; x++){
            for(int y = 0; y < _boardSize; y++){
                if (_board[x][y] == src) _board[x][y] = dest;
            }
        }
    }

    //---- Methods for players and moves ----//

    public int getPlayerCount() {
        return _players.length;
    }

    public Player getPlayer(int i) {
        return _players[i];
    }

    public Player getCurrentPlayer() {
        return getPlayerAtMove(_currentMove);
    }

    public int getCurrentMove() {
        return _currentMove;
    }

    public Player getPlayerAtMove(int moveIndex) {
        if(moveIndex < 0 || moveIndex >= _maxMove) return null;
        return _players[moveIndex % _players.length];
    }

    //---- Methods for moves and scoring ----//

    private void select(int x, int y) {
        _selected.x = x;
        _selected.y = y;
        _selectedSquares = locateSquares(_selected);
    }

    public Point getSelected() {
        return _selected;
    }

    public Square[] getSelectedSquares(){
        return _selectedSquares;
    }

    public boolean processInput(int x, int y){
        Point p = new Point(x, y);
        Util.logVerbose(this, "processInput: "+x+", "+y);
        Player player = getCurrentPlayer();

        if(!isValidLocation(x, y)) {
            Util.logDebug(this, "processInput: ("+x+", "+y+") "+" invalid location.");
            return false;
        }

        if(player != null && player.isLocalHuman() &&
           isFree(x,y) && p.equals(_selected)) {
            Util.logDebug(this, "processInput: ("+x+", "+y+") "+" input accepted.");
            makeMove(player, x, y);
        } else {
            select(x, y);
        }
        return true;
    }

    public boolean isComputerMove() {
        Player p = getCurrentPlayer();
        return p != null && !p.isLocalHuman();
    }

    public boolean makeMove(final Player player, int x, int y) {
        if(!isFree(x, y)) return false;
        int move = _currentMove;

        // Erase any other square that had this value (which can
        // happen as a result of undo, for example).
        replacePieces(_currentMove, _maxMove);
        // Set our new piece.
        _board[x][y] = move;
        // Advance turn.
        _currentMove++;

        // Select the square (which also calculates the formed squares).
        select(x, y);

        // Evaluate the points, and add to the player's score.
        int points = evaluateSquares(_selectedSquares);
        player.incrementScore(points);
        _moves.add(new Move(_selected, points));
        Util.logInfo(this, "makeMove: move = "+move+" score = "+player.getScore());

        return true;
    }

    private int evaluateSquares(final Square[] squares){
        int total = 0;
        for (Square sq : squares) {
            // The squares we constuct will always have integer areas,
            // so we don't lose any accuracy here.
            total += (int)Math.round(sq.calcArea());
        }
        return total;
    }

    private Square[] locateSquares(final Point a){
        Player p = getPlayerAtLoc(a);
        if(p == null) return new Square[0];
        ArrayList<Square> result = new ArrayList<Square>();
        for(int x = 0; x < _boardSize; x++){
            for(int y = 0; y < _boardSize; y++){
                Point b = new Point(x, y);

                // skip self
                if(a.equals(b))
                    continue;

                // ignore other pieces
                if(getPlayerAtLoc(b) != p)
                    continue;

                // points a and b form one of the 4 sides of the square
                // there are 2 possible set for points c and d
                // since we are only looking for fully formed squares
                // and looping thru the whole board
                // we will be able to catch them all using this simple calculation
                // which would not work other wise
                int dx = b.x - a.x;
                int dy = b.y - a.y;
                Point c = new Point(b.x - dy, b.y + dx);
                Point d = new Point(a.x - dy, a.y + dx);

                if(getPlayerAtLoc(c) == p &&
                   getPlayerAtLoc(d) == p) {
                    result.add(new Square(a, b, c, d));
                }
            }
        }
        return result.toArray(new Square[result.size()]);
    }

    // ------------------------------------------------------------------------
    // Static implementation.
    // ------------------------------------------------------------------------

    private static int[][] to2DBoard(int[] boardArray){
        int boardSize = (int)Math.sqrt(boardArray.length);
        int[][] board = new int[boardSize][boardSize];
        int index = 0;
        for(int r = 0; r < boardSize; r++){
            System.arraycopy(boardArray, index, board[r], 0, boardSize);
            index+=boardSize;
        }
        return board;
    }

    private static int[] to1DBoard(int[][] board){
        int boardSize = board.length;
        int[] boardArray = new int[boardSize * boardSize];
        int index = 0;
        for(int r = 0; r < boardSize; r++){
            System.arraycopy(board[r], 0, boardArray, index, boardSize);
            index+=boardSize;
        }
        return boardArray;
    }

    public static class Square {
        public final Point a, b, c, d;

        // The points must be listed in CW or CCW order.
        public Square(Point a, Point b, Point c, Point d){
            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
        }

        public float calcArea(){
            // Here we assume that the points are a perfect square.
            return Util.sqDistF(a, b);
        }
    }

    public static class Move implements Parcelable{
        public final Point p;
        public final int points;

        public Move(Point p, int points) {
            this.p = p;
            this.points = points;
        }

        @Override
        public String toString(){
            return "("+p.x+","+p.y+")="+points;
        }

        // ------------------------------------------------------------------------
        // Parcelable implementation.
        // ------------------------------------------------------------------------

        public Move(Parcel in){
            p = new Point();
            p.x = in.readInt();
            p.y = in.readInt();
            points = in.readInt();
        }

        public void writeToParcel(Parcel out, int flag) {
            out.writeInt(p.x);
            out.writeInt(p.y);
            out.writeInt(points);
        }

        public int describeContents() { // TODO: what the heck is this for
            return 0;
        }

        public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
            public Move createFromParcel(Parcel in) { 
                return new Move(in);
            }

            public Move[] newArray(int size) {
                return new Move[size];
            }
        };
    }
}
